package edu.ssdut.revise.core.matchRule;

import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.nodeTypes.NodeWithStatements;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.utils.Pair;
import edu.ssdut.revise.core.match.MatchStore;
import info.debatty.java.stringsimilarity.QGram;

import java.util.*;

import static java.lang.Math.max;


public class Bottom2UpMatchRule implements MatchRule {
    private final double THRESHOLD = 0.7;

    @Override
    public void typeDeclarationMatch(MatchStore<TypeDeclaration<?>> matchStore) {
        // TODO 如何有效利用下层子结点匹配信息？
    }

    @Override
    public void variableDeclaratorMatch(MatchStore<VariableDeclarator> matchStore) {
        // find best match
        Iterator<VariableDeclarator> iter1 = matchStore.getUnmatchedNodes1().iterator();
        while (iter1.hasNext()) {
            VariableDeclarator unmatchedStates1 = iter1.next();
            Iterator<VariableDeclarator> iter2 = matchStore.getUnmatchedNodes2().iterator();

            double BestMatchRatio = THRESHOLD;
            VariableDeclarator bestMatchIn2 = null;

            while (iter2.hasNext()) {
                VariableDeclarator unmatchedStates2 = iter2.next();
                double thisRatio = this.getVariableDeclaratorMatchRatio(unmatchedStates1, unmatchedStates2);
                if (thisRatio > BestMatchRatio) {
                    BestMatchRatio = thisRatio;
                    bestMatchIn2 = unmatchedStates2;
                }
            }
            // remove best match
            // add best match to matchStore
            if (bestMatchIn2 != null) {
                matchStore.getMatched().add(new Pair<>(unmatchedStates1, bestMatchIn2));
                iter1.remove();
                matchStore.deleteUnmatchedNodes2(bestMatchIn2);
            }
        }
    }

    @Override
    public void methodDeclarationMatch(MatchStore<MethodDeclaration> matchStore) {
        // find best match
        Iterator<MethodDeclaration> iter1 = matchStore.getUnmatchedNodes1().iterator();
        while (iter1.hasNext()) {
            MethodDeclaration unmatchedStates1 = iter1.next();
            Iterator<MethodDeclaration> iter2 = matchStore.getUnmatchedNodes2().iterator();

            double BestMatchRatio = THRESHOLD;
            MethodDeclaration bestMatchIn2 = null;

            while (iter2.hasNext()) {
                MethodDeclaration unmatchedStates2 = iter2.next();
                double thisRatio = this.getMethodDeclarationMatchRatio(unmatchedStates1, unmatchedStates2);
                if (thisRatio > BestMatchRatio) {
                    BestMatchRatio = thisRatio;
                    bestMatchIn2 = unmatchedStates2;
                }
            }
            // remove best match
            // add best match to matchStore
            if (bestMatchIn2 != null) {
                matchStore.getMatched().add(new Pair<>(unmatchedStates1, bestMatchIn2));
                iter1.remove();
                matchStore.deleteUnmatchedNodes2(bestMatchIn2);
            }
        }
    }

    @Override
    public void constructorDeclarationMatch(MatchStore<ConstructorDeclaration> matchStore) {

    }

    //public Boolean ifTypeDeclarationMatch(TypeDeclaration<?> a, TypeDeclaration<?> b) {
    //    return null;
    //}

    public Double getVariableDeclaratorMatchRatio(VariableDeclarator a, VariableDeclarator b) {
        if (!a.getTypeAsString().equals(b.getTypeAsString())) {
            return 0.0;
        }
        return this.textSim(a.getNameAsString(), b.getNameAsString());
    }

    public Double getMethodDeclarationMatchRatio(MethodDeclaration a, MethodDeclaration b) {
        // require the same type
        if (!Objects.equals(a.getTypeAsString(), b.getTypeAsString())) return 0.0;

        if (a.getBody().isPresent() && b.getBody().isPresent()) {
            // neither empty
            return rucursiveGetStmtMatchRatio(a.getBody().get(), b.getBody().get());
        } else {
            // both empty, return best name match (for interfaces maybe?)
            if (a.getBody().isPresent() == b.getBody().isPresent()) {
                return textSim(a.getNameAsString(), b.getNameAsString());
            } else return 0.0;
        }
    }

    /**
     * return the similarity ratio between 2 given statement nodes, with or without descendent statements.
     * first check if statement nodes are of the same type, then for NodeWithStatements(inner statements), return the similarity of children,
     * for node with no statements(leaf statements), return the text similarity.
     */
    private Double rucursiveGetStmtMatchRatio(Statement s1, Statement s2) {
        if (!ifStmtIdenticalType(s1, s2)) return 0.0;   // 类型不同
        else {
            if (s1 instanceof NodeWithStatements && s2 instanceof NodeWithStatements) {
                NodeWithStatements<?> s1WithStmts = (NodeWithStatements<?>) s1;
                NodeWithStatements<?> s2WithStmts = (NodeWithStatements<?>) s2;
                double matchCount = 0d;
                MatchStore<Statement> statementMatchStore = new MatchStore<>();
                // init matchStore
                for (int i = 0; i < s1WithStmts.getStatements().size(); i++) {
                    Statement innerS1 = s1WithStmts.getStatements().get(i);
                    statementMatchStore.insertUnmatchedNodes1(innerS1);
                }
                for (int i = 0; i < s2WithStmts.getStatements().size(); i++) {
                    Statement innerS2 = s2WithStmts.getStatements().get(i);
                    statementMatchStore.insertUnmatchedNodes2(innerS2);
                }
                // get best match
                Iterator<Statement> iter1 = statementMatchStore.getUnmatchedNodes1().iterator();
                while (iter1.hasNext()) {
                    Statement unmatchedStates1 = iter1.next();
                    Iterator<Statement> iter2 = statementMatchStore.getUnmatchedNodes2().iterator();

                    double BestMatchRatio = THRESHOLD;
                    Statement bestMatchIn2 = null;

                    while (iter2.hasNext()) {
                        Statement unmatchedStates2 = iter2.next();
                        double thisRatio = this.rucursiveGetStmtMatchRatio(unmatchedStates1, unmatchedStates2);
                        if (thisRatio > BestMatchRatio) {
                            BestMatchRatio = thisRatio;
                            bestMatchIn2 = unmatchedStates2;
                        }
                    }
                    // remove best match
                    // no need to add to matches
                    if (bestMatchIn2 != null) {
                        matchCount++;
                        iter1.remove();
                        statementMatchStore.deleteUnmatchedNodes2(bestMatchIn2);
                    }
                }
                // 返回ratio
                return matchCount / max(s1WithStmts.getStatements().size(), s2WithStmts.getStatements().size());
            } else if (!(s1 instanceof NodeWithStatements) && !(s2 instanceof NodeWithStatements)) {
                // 字符串相似度
                return textSim(s1.toString(), s2.toString());
            } else {
                // 类型不同，目前直接返回0处理
                return 0d;
            }
        }
    }

    private boolean ifStmtIdenticalType(Statement s1, Statement s2) {
        if (s1.isBlockStmt() && s2.isBlockStmt()) return true;
        if (s1.isBreakStmt() && s2.isBreakStmt()) return true;
        if (s1.isIfStmt() && s2.isIfStmt()) return true;
        if (s1.isForStmt() && s2.isForStmt()) return true;
        if (s1.isWhileStmt() && s2.isWhileStmt()) return true;
        if (s1.isReturnStmt() && s2.isReturnStmt()) return true;
        if (s1.isSwitchStmt() && s2.isSwitchStmt()) return true;
        if (s1.isContinueStmt() && s2.isContinueStmt()) return true;
        if (s1.isThrowStmt() && s2.isThrowStmt()) return true;
        if (s1.isTryStmt() && s2.isTryStmt()) return true;
        if (s1.isDoStmt() && s2.isDoStmt()) return true;
        if (s1.isEmptyStmt() && s2.isEmptyStmt()) return true;
        if (s1.isExplicitConstructorInvocationStmt() && s2.isExplicitConstructorInvocationStmt()) return true;
        if (s1.isExpressionStmt() && s2.isExpressionStmt()) return true;
        if (s1.isForEachStmt() && s2.isForEachStmt()) return true;
        if (s1.isLabeledStmt() && s2.isLabeledStmt()) return true;
        if (s1.isLocalClassDeclarationStmt() && s2.isLocalClassDeclarationStmt()) return true;
        if (s1.isLocalRecordDeclarationStmt() && s2.isLocalRecordDeclarationStmt()) return true;
        if (s1.isYieldStmt() && s2.isYieldStmt()) return true;
        //if(s1.isUnparsableStmt() && s2.isUnparsableStmt()) return true;
        return false;
    }

    public double textSim(String a, String b) {
        a = a.toLowerCase();
        b = b.toLowerCase();    // 提升变量名顺序调整匹配

        // dice coefficient with bi-grams
        QGram qgram = new QGram(2);
        Map<String, Integer> profileA = qgram.getProfile(a);
        Map<String, Integer> profileB = qgram.getProfile(b);

        int intersectionLen;
        Set<String> set = new HashSet<>(profileA.keySet());
        set.retainAll(profileB.keySet());
        intersectionLen = set.stream().mapToInt(str -> Math.min(profileA.get(str), profileB.get(str))).sum();

        int sumA = a.length() - 1;   // biGram !important
        int sumB = b.length() - 1;   // biGram !important

        return intersectionLen * 2.0 / (sumA + sumB);
    }
}


